/*
 * Unidata Platform Community Edition
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 * This file is part of the Unidata Platform Community Edition software.
 *
 * Unidata Platform Community Edition is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Unidata Platform Community Edition is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */

package org.unidata.mdm.meta.util;

import static java.util.stream.Collectors.toCollection;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.unidata.mdm.core.type.model.AttributeElement;
import org.unidata.mdm.meta.service.impl.data.instance.AttributeImpl;
import org.unidata.mdm.meta.type.model.DataModel;
import org.unidata.mdm.meta.type.model.MetaModelAttribute;
import org.unidata.mdm.meta.type.model.OrderedElement;
import org.unidata.mdm.meta.type.model.attributes.ArrayMetaModelAttribute;
import org.unidata.mdm.meta.type.model.attributes.CodeMetaModelAttribute;
import org.unidata.mdm.meta.type.model.attributes.ComplexMetaModelAttribute;
import org.unidata.mdm.meta.type.model.attributes.SimpleMetaModelAttribute;
import org.unidata.mdm.meta.type.model.entities.AbstractEntity;
import org.unidata.mdm.meta.type.model.entities.ComplexAttributesHolderEntity;
import org.unidata.mdm.meta.type.model.entities.EntitiesGroup;
import org.unidata.mdm.meta.type.model.entities.Entity;
import org.unidata.mdm.meta.type.model.entities.LookupEntity;
import org.unidata.mdm.meta.type.model.entities.NestedEntity;
import org.unidata.mdm.meta.type.model.entities.Relation;
import org.unidata.mdm.meta.type.model.enumeration.EnumerationType;
import org.unidata.mdm.meta.type.model.enumeration.EnumerationValue;
import org.unidata.mdm.meta.type.model.merge.MergeAttribute;
import org.unidata.mdm.meta.type.model.merge.MergeSettings;
import org.unidata.mdm.meta.type.model.sourcesystem.SourceSystem;
import org.unidata.mdm.system.util.TextUtils;

/**
 * @author Mikhail Mikhailov
 */
public class ModelUtils {

    public static final String ESCAPE_STANDARD_NAMESPACE_SEPARATOR = "\\.";

    private static final int DIRECT_LEVEL = 1;
    /**
     * Default model instace id.
     */
    public static final String DEFAULT_MODEL_INSTANCE_ID = "default";
    /**
     * Default validation pattern for names in model.
     */
    public static final Pattern DEFAULT_MODEL_NAME_PATTERN = Pattern.compile("^[a-z][a-z0-9_-]*$", Pattern.CASE_INSENSITIVE);
    /**
     * Name of the default source system.
     */
    public static final String DEFAULT_SOURCE_SYSTEM_NAME = "unidata";
    /**
     * Weight of the default source system.
     */
    public static final int DEFAULT_SOURCE_SYSTEM_WEIGHT = 100;
    /**
     * Name of the default entities group.
     */
    public static final String DEFAULT_GROUP_NAME = "ROOT";
    /**
     * Default root group.
     */
    public static final EntitiesGroup DEFAULT_ROOT_GROUP = new EntitiesGroup()
        .withName(DEFAULT_GROUP_NAME)
        .withDisplayName("change.default.group.name");
    /**
     * Source systems comparator.
     */
    public static final Comparator<SourceSystem> SOURCE_SYSTEMS_ASCENDING_COMPARATOR
            = Comparator.comparingInt(o -> o.getWeight().intValue());
    /**
     * Source systems comparator.
     */
    public static final Comparator<SourceSystem> SOURCE_SYSTEMS_DESCENDING_COMPARATOR
            = (o1, o2) -> o2.getWeight().intValue() - o1.getWeight().intValue();

    /**
     * Displayable attributes comparator.
     */
    public static final Comparator<? super MetaModelAttribute> DISPLAYABLE_ATTRIBUTES_COMPARATOR
            = Comparator.comparingInt(o -> (o.getClass().isAssignableFrom(OrderedElement.class) ? ((OrderedElement) o).getOrder() : 0));

    /**
     * Instantiation disabled.
     */
    private ModelUtils() {
        super();
    }

    // UN-7293
    /*
    public static void calculateDqRuleId(DQRuleDef dqRuleDefinition) {
        if (StringUtils.isEmpty(dqRuleDefinition.getId())) {
            dqRuleDefinition.setId(IdUtils.v4String());
        }
    }
    */

    /**
     * Splits the path using standard '.' separator.
     * @param path the path to split
     * @return split
     */
    public static String[] splitPath(String path) {
        return StringUtils.split(path, '.');
    }

    /**
     * Splits the path using standard '.' separator.
     * @param path the path to split
     * @return split
     */
    public static String joinPath(String path, String tail) {
        if (StringUtils.isBlank(path)) {
            return tail;
        } else {
            return StringUtils.joinWith(".", path, tail);
        }
    }

    /**
     * Gets attribute name respecting level.
     *
     * @param level the level
     * @param path the path
     * @return attribute name
     */
    public static String getAttributeName(int level, String path) {
        return StringUtils.split(path, '.')[level];
    }

    /**
     * Tests if the name is a compound path.
     *
     * @param path property name
     * @return true, if so, false otherwise
     */
    public static boolean isCompoundPath(String path) {
        return path != null && path.indexOf('.') != -1;
    }

    /**
     * Strips attribute path according to the level.
     *
     * @param level the level
     * @param path the path
     * @return attribute path
     */
    public static String stripAttributePath(int level, String path) {
        return String.join(".", Arrays.copyOf(StringUtils.split(path, '.'), level + 1));
    }

    /**
     * Strips attribute path according to the level.
     *
     * @param level the level
     * @param path the path
     * @return attribute path
     */
    public static String subAttributePath(int level, String path) {
        String[] parts = StringUtils.split(path, '.');
        return String.join(".", Arrays.copyOfRange(parts, level, parts.length));
    }

    /**
     * @param path the path
     * @return level of attribute in hierarchy
     */
    public static int getAttributeLevel(String path) {
        String[] parts = StringUtils.split(path, '.');
        return parts.length - 1;
    }

    /**
     * Gets path for an attribute.
     *
     * @param level current level
     * @param path current path
     * @param attr attribute
     * @return joined path
     */
    public static String getAttributePath(int level, String path, MetaModelAttribute attr) {
        return getAttributePath(level, path, attr.getName());
    }

    /**
     * Gets path for an attribute.
     *
     * @param level current level
     * @param path current path
     * @param attrName attribute name
     * @return joined path
     */
    public static String getAttributePath(int level, String path, String attrName) {
        return level == 0 ? attrName : String.join(".", path, attrName);
    }

    /**
     * Gets path for an attribute.
     *
     * @param path current path
     * @param attrName attribute
     * @return joined path
     */
    public static String getAttributePath(String path, String attrName) {
        return StringUtils.isBlank(path) ? attrName : String.join(".", path, attrName);
    }

    /**
     * Creates (linked) enumeration map.
     *
     * @param enumeration the enumeration to process
     * @return map with [name, displayName] entries
     */
    @Deprecated(forRemoval = true)
    public static Map<String, String> createEnumerationMap(EnumerationType enumeration) {

        if (enumeration == null || enumeration.getValues() == null || enumeration.getValues().isEmpty()) {
            return Collections.emptyMap();
        }

        Map<String, String> enumerationMap = new LinkedHashMap<>(enumeration.getValues().size());
        for (EnumerationValue enumValue : enumeration.getValues()) {
            enumerationMap.put(enumValue.getName(), enumValue.getDisplayName());
        }

        return enumerationMap;
    }

    /**
     * Creates (linked) source systems map.
     *
     * @param sourceSystems
     * @param descending returns map in descending weight order if true, otherwise ascending
     * @return map
     */
    public static Map<String, Integer> createSourceSystemsMap(List<SourceSystem> sourceSystems, boolean descending) {

        if (sourceSystems == null || sourceSystems.isEmpty()) {
            return Collections.emptyMap();
        }

        List<SourceSystem> copy = new ArrayList<>(sourceSystems);
        Collections.sort(copy, descending ? SOURCE_SYSTEMS_DESCENDING_COMPARATOR : SOURCE_SYSTEMS_ASCENDING_COMPARATOR);

        Map<String, Integer> sourceSystemsMap = new LinkedHashMap<>(copy.size());
        for (SourceSystem ssd : copy) {
            sourceSystemsMap.put(ssd.getName(), ssd.getWeight());
        }

        return sourceSystemsMap;
    }

    /**
     * Creates BVT attributes map.
     *
     * @param e the entity
     * @param globalSourceSystems global source systems list
     * @param attrs attributes map
     */
    public static Map<String, Map<String, Integer>> createBvtMap(
            AbstractEntity<?> e,
            Map<String, Integer> globalSourceSystemsMap,
            Map<String, AttributeElement> attrs) {

        MergeSettings settings = e.getMergeSettings();
        List<MergeAttribute> bvtAttrs
                = settings != null && settings.getBvtSettings() != null
                ? settings.getBvtSettings().getAttributes()
                : null;

        Map<String, Map<String, Integer>> mergeAttrs = new LinkedHashMap<>();
        for (int i = 0; bvtAttrs != null && i < bvtAttrs.size(); i++) {

            MergeAttribute attrDef = bvtAttrs.get(i);
            Map<String, Integer> overridden = ModelUtils.createSourceSystemsMap(attrDef.getSourceSystemsConfigs(), true);

            // UN-3053 Merge settings doesn't contain new source systems
            for (Entry<String, Integer> ge : globalSourceSystemsMap.entrySet()) {
                if (overridden.containsKey(ge.getKey())) {
                    continue;
                }

                overridden.put(ge.getKey(), 0);
            }

            mergeAttrs.put(attrDef.getName(), overridden);
        }

        Map<String, Integer> bvrSourceSystemsMap
                = ModelUtils.createSourceSystemsMap(settings != null && settings.getBvrSettings() != null
                ? settings.getBvrSettings().getSourceSystemsConfigs()
                : null, true);

        for (Entry<String, AttributeElement> entry : attrs.entrySet()) {
            if (mergeAttrs.containsKey(entry.getKey())) {
                continue;
            }

            if (!bvrSourceSystemsMap.isEmpty()) {
                mergeAttrs.put(entry.getKey(), bvrSourceSystemsMap);
                continue;
            }

            mergeAttrs.put(entry.getKey(), globalSourceSystemsMap);
        }

        return mergeAttrs;
    }

    /**
     * Returns new ordered attributes map.
     *
     * @param e entity
     * @param refs references
     * @return map
     */
    public static Map<String, AttributeElement> createAttributesMap(AbstractEntity<?> e, List<NestedEntity> refs) {

        Map<String, AttributeElement> attrs = new LinkedHashMap<>();
        createAttributesMap(e, StringUtils.EMPTY, 0, refs, attrs, null);
        return attrs;
    }

    /**
     * Builds an attributes map for an entity recursively.
     *
     * @param e the entity
     * @param path current path
     * @param level current level
     * @param refs nested entities (references)
     * @param attrs attributes map
     * @param parent parent link
     */
    public static void createAttributesMap(
        AbstractEntity<?> e, String path, int level,
        List<NestedEntity> refs,
        Map<String, AttributeElement> attrs, AttributeElement parent) {

        // 1. Check lookup entity attributes
        if (e instanceof LookupEntity) {

            LookupEntity lookupEntityDef = (LookupEntity) e;
            CodeMetaModelAttribute attr = lookupEntityDef.getCodeAttribute();
            AttributeElement holder = new AttributeImpl(attr, e, null, getAttributePath(level, path, attr), level, false);
            attrs.put(holder.getPath(), holder);

            for (CodeMetaModelAttribute attributeDef : lookupEntityDef.getAliasCodeAttributes()) {
                AttributeElement alias
                        = new AttributeImpl(attributeDef, e, null, getAttributePath(level, path, attributeDef), level, true);
                attrs.put(alias.getPath(), alias);
            }
        }

        // 2. Process simple attributes
        List<AttributeElement> thisLevelAttrs = new ArrayList<>(
            e.getArrayAttribute().size() +
                e.getSimpleAttribute().size());

        for (SimpleMetaModelAttribute attr : e.getSimpleAttribute()) {
            if (StringUtils.isBlank(attr.getName())) {
                throw new IllegalArgumentException("Name of a simple attribute is invalid.");
            }

            AttributeElement holder = new AttributeImpl(attr, e, parent, getAttributePath(level, path, attr), level);
            if (parent != null) {
                parent.getChildren().add(holder);
            }

            thisLevelAttrs.add(holder);
        }

        for (ArrayMetaModelAttribute attr : e.getArrayAttribute()) {
            if (StringUtils.isBlank(attr.getName())) {
                throw new IllegalArgumentException("Name of an array attribute is invalid.");
            }

            AttributeElement holder = new AttributeImpl(attr, e, parent, getAttributePath(level, path, attr), level);
            if (parent != null) {
                parent.getChildren().add(holder);
            }

            thisLevelAttrs.add(holder);
        }

        thisLevelAttrs.sort(Comparator.comparingInt(AttributeElement::getOrder));
        for (AttributeElement holder : thisLevelAttrs) {
            attrs.put(holder.getPath(), holder);
        }

        // 3. Process complex attributes
        if (e instanceof ComplexAttributesHolderEntity) {

            ComplexAttributesHolderEntity<?> cahe = (ComplexAttributesHolderEntity<?>) e;
            for (ComplexMetaModelAttribute attr : cahe.getComplexAttribute()) {

                if (StringUtils.isBlank(attr.getName())
                 || StringUtils.isBlank(attr.getNestedEntityName())) {
                    throw new IllegalArgumentException("Name or nested entity name of a complex attribute is invalid.");
                }

                AttributeElement holder = new AttributeImpl(attr, e, parent, getAttributePath(level, path, attr), level);
                if (parent != null) {
                    parent.getChildren().add(holder);
                }

                attrs.put(holder.getPath(), holder);
                NestedEntity nested = refs.stream()
                        .filter(ne -> attr.getNestedEntityName().equals(ne.getName()))
                        .findFirst()
                        .orElse(null);

                createAttributesMap(nested, holder.getPath(), level + 1, refs, attrs, holder);
            }
        }
    }

    public static Collection<String> findAllTopLevelNames(DataModel model) {

        Collection<String> allNames = new ArrayList<>();
        if (Objects.nonNull(model.getEntitiesGroup())) {
            allNames.add(model.getEntitiesGroup().getName());
        }

        model.getNestedEntities().stream().filter(Objects::nonNull).map(NestedEntity::getName).collect(toCollection(() -> allNames));
        model.getEntities().stream().filter(Objects::nonNull).map(Entity::getName).collect(toCollection(() -> allNames));
        model.getLookupEntities().stream().filter(Objects::nonNull).map(LookupEntity::getName).collect(toCollection(() -> allNames));
        model.getRelations().stream().filter(Objects::nonNull).map(Relation::getName).collect(toCollection(() -> allNames));

        return allNames;
    }

    /**
     * Find attribute.
     *
     * @param pathToSearch the path to search
     * @param entity the entity
     * @return the simple attribute
     */
    @Nullable
    public static MetaModelAttribute findModelAttribute(@Nonnull String pathToSearch, @Nonnull AbstractEntity<?> entity, Collection<NestedEntity> nestedEntityDefs) {
        String[] splitPath = StringUtils.split(pathToSearch, ESCAPE_STANDARD_NAMESPACE_SEPARATOR);
        return findModelAttributeBySplitPath(splitPath, entity, nestedEntityDefs);
    }

    @Nullable
    private static MetaModelAttribute findModelAttributeBySplitPath(@Nonnull final String[] splitPath, @Nonnull final AbstractEntity<?> entity, Collection<NestedEntity> nestedEntityDefs) {

        if (ArrayUtils.isEmpty(splitPath)) {
            return null;
        }

        String attrName = splitPath[0];
        if (splitPath.length == DIRECT_LEVEL) {
            return getAttributeByName(entity, attrName);
        } else if (!(entity instanceof ComplexAttributesHolderEntity)) {
            return null;
        }

        MetaModelAttribute abstractAttribute = getAttributeByName((ComplexAttributesHolderEntity<?>) entity, attrName);

        String nestedEntityName;
        if (abstractAttribute == null) {
            nestedEntityName = StringUtils.EMPTY;
        } else {
            nestedEntityName = (abstractAttribute instanceof ComplexMetaModelAttribute)
                ? ((ComplexMetaModelAttribute) abstractAttribute).getNestedEntityName()
                : abstractAttribute.getName();
        }

        Optional<? extends AbstractEntity<?>> foundNestedEntity = nestedEntityDefs.stream()
            .filter(nestedEntity -> nestedEntity.getName().equals(nestedEntityName))
            .findAny();

        String[] nextPath = Arrays.copyOfRange(splitPath, 1, splitPath.length);
        return !foundNestedEntity.isPresent() ? null : findModelAttributeBySplitPath(nextPath, foundNestedEntity.get(), nestedEntityDefs);
    }

    /**
     * @param entity - entity for searching
     * @param attrName - attribute name
     * @return attribute from top level of entity if it present
     */
    @Nullable
    public static MetaModelAttribute getAttributeByName(@Nonnull final AbstractEntity<?> entity, @Nonnull final String attrName) {
        if (entity instanceof LookupEntity) {
            return getAttributeByName((LookupEntity) entity, attrName);
        } else if (entity instanceof ComplexAttributesHolderEntity) {
            return getAttributeByName((ComplexAttributesHolderEntity<?>) entity, attrName);
        } else {
            return getAttributeByNameFromSimpleAttrHolder(entity, attrName);
        }
    }

    /**
     * @param entity - entity for searching
     * @param attrName - attribute name
     * @return attribute from top level of entity if it present
     */
    @Nullable
    public static MetaModelAttribute getAttributeByName(@Nonnull final LookupEntity entity, @Nonnull final String attrName) {
        Optional<CodeMetaModelAttribute> codeAttribute = entity.getAliasCodeAttributes().stream()
                .filter(attr -> attrName.equals(attr.getName())).findAny();
        if (codeAttribute.isPresent()) {
            return codeAttribute.get();
        } else if (entity.getCodeAttribute().getName().equals(attrName)) {
            return entity.getCodeAttribute();
        } else {
            return getAttributeByNameFromSimpleAttrHolder(entity, attrName);
        }
    }

    /**
     * @param entity - entity for searching
     * @param attrName - attribute name
     * @return attribute from top level of entity if it present
     */
    @Nullable
    public static MetaModelAttribute getAttributeByName(@Nonnull final ComplexAttributesHolderEntity<?> entity, @Nonnull final String attrName) {

        Optional<? extends MetaModelAttribute> foundComplexAttr = entity.getComplexAttribute().stream()
                .filter(attributeDef -> attributeDef.getName().equals(attrName))
                .findAny();

        if (foundComplexAttr.isPresent()) {
            return foundComplexAttr.get();
        } else {
            return getAttributeByNameFromSimpleAttrHolder(entity, attrName);
        }
    }

    private static MetaModelAttribute getAttributeByNameFromSimpleAttrHolder(@Nonnull final AbstractEntity<?> entity, @Nonnull final String attrName) {

        Optional<? extends MetaModelAttribute> foundAttr = entity.getSimpleAttribute().stream()
                .filter(attr -> attr.getName().equals(attrName))
                .findAny();

        if (foundAttr.isPresent()) {
            return foundAttr.get();
        } else {
            return entity.getArrayAttribute().stream()
                    .filter(attr -> attr.getName().equals(attrName))
                    .findAny()
                    .orElse(null);
        }
    }

    /**
     * @param entity - entity for searching
     * @param attrName - attribute name
     * @return boolean value. true mean it is complex attribute name, false mean attribute not presented or it is simple attribute.
     */
    public static boolean isComplexAttribute(@Nonnull final ComplexAttributesHolderEntity<?> entity, @Nonnull final String attrName) {
        MetaModelAttribute result = getAttributeByName(entity, attrName);
        return result instanceof ComplexMetaModelAttribute;
    }

    /**
     * Creates default (system) source - system.
     *
     * @return source systen definition
     */
    public static SourceSystem createDefaultSourceSystem() {
        return new SourceSystem()
                .withName(ModelUtils.DEFAULT_SOURCE_SYSTEM_NAME)
                .withWeight(ModelUtils.DEFAULT_SOURCE_SYSTEM_WEIGHT)
                .withAdmin(true)
                .withDisplayName(TextUtils.getText("app.meta.default.source.system"));
    }

    /**
     * Creates default (system) root entities group.
     *
     * @return entities group
     */
    public static EntitiesGroup createDefaultEntitiesGroup() {
        return new EntitiesGroup()
                .withName(DEFAULT_GROUP_NAME)
                .withDisplayName(TextUtils.getText("app.meta.default.entities.group.root"));
    }

    /**
     * Creates a new root cleanse function group and system cleanse functions.
     * @return
     */
    /*
    public static CleanseFunctionGroupDef createDefaultCleanseFunctionGroup(Locale locale) {

        Map<String, List<String>> functionsMap = new HashMap<>();

        ResourceBundle bundle = ResourceBundle.getBundle("cleanse", locale, new UTF8Control());

        for (String k : bundle.keySet()) {
            String v = bundle.getString(k);
            if (processCleanseFunctionInfo(functionsMap, "definition", k, v)
                    || processCleanseFunctionInfo(functionsMap, "className", k, v)
                    || processCleanseFunctionInfo(functionsMap, "functionName", k, v)) {
                continue; // Bogus
            }
        }

        final String packagePrefix = "com.unidata.mdm.cleanse";
        CleanseFunctionGroupDef root = DqJaxbUtils.getMetaObjectFactory().createCleanseFunctionGroupDef()
                .withGroupName(MessageUtils.getMessageWithLocaleAndDefault(locale, "app.meta.default.cleanse.functions.group.root.name", "app.meta.default.cleanse.functions.group.root.name"))
                .withDescription(MessageUtils.getMessageWithLocaleAndDefault(locale, "app.meta.default.cleanse.functions.group.root.description", "app.meta.default.cleanse.functions.group.root.description"))
                .withVersion(1L);
        CleanseFunctionGroupDef string = DqJaxbUtils.getMetaObjectFactory().createCleanseFunctionGroupDef()
                .withGroupName(MessageUtils.getMessageWithLocaleAndDefault(locale, "app.meta.default.cleanse.functions.group.string.name", "app.meta.default.cleanse.functions.group.string.name"))
                .withDescription(MessageUtils.getMessageWithLocaleAndDefault(locale, "app.meta.default.cleanse.functions.group.string.description", "app.meta.default.cleanse.functions.group.string.description"))
                .withVersion(1L);
        CleanseFunctionGroupDef math = DqJaxbUtils.getMetaObjectFactory().createCleanseFunctionGroupDef()
                .withGroupName(MessageUtils.getMessageWithLocaleAndDefault(locale, "app.meta.default.cleanse.functions.group.math.name", "app.meta.default.cleanse.functions.group.math.name"))
                .withDescription(MessageUtils.getMessageWithLocaleAndDefault(locale, "app.meta.default.cleanse.functions.group.math.description", "app.meta.default.cleanse.functions.group.math.description"))
                .withVersion(1L);
        CleanseFunctionGroupDef logic = DqJaxbUtils.getMetaObjectFactory().createCleanseFunctionGroupDef()
                .withGroupName(MessageUtils.getMessageWithLocaleAndDefault(locale, "app.meta.default.cleanse.functions.group.logic.name","app.meta.default.cleanse.functions.group.logic.name"))
                .withDescription(MessageUtils.getMessageWithLocaleAndDefault(locale, "app.meta.default.cleanse.functions.group.logic.description", "app.meta.default.cleanse.functions.group.logic.description"))
                .withVersion(1L);
        CleanseFunctionGroupDef convert = DqJaxbUtils.getMetaObjectFactory().createCleanseFunctionGroupDef()
                .withGroupName(MessageUtils.getMessageWithLocaleAndDefault(locale, "app.meta.default.cleanse.functions.group.convert.name", "app.meta.default.cleanse.functions.group.convert.name"))
                .withDescription(MessageUtils.getMessageWithLocaleAndDefault(locale, "app.meta.default.cleanse.functions.group.convert.description", "app.meta.default.cleanse.functions.group.convert.description"))
                .withVersion(1L);
        CleanseFunctionGroupDef misc = DqJaxbUtils.getMetaObjectFactory().createCleanseFunctionGroupDef()
                .withGroupName(MessageUtils.getMessageWithLocaleAndDefault(locale, "app.meta.default.cleanse.functions.group.misc.name", "app.meta.default.cleanse.functions.group.misc.name"))
                .withDescription(MessageUtils.getMessageWithLocaleAndDefault(locale, "app.meta.default.cleanse.functions.group.misc.description", "app.meta.default.cleanse.functions.group.misc.description"))
                .withVersion(1L);

        Date atDate = new Date();
        for (Entry<String, List<String>> entry : functionsMap.entrySet()) {

            if (entry.getKey().startsWith(String.join(".", packagePrefix, "string"))) {
                string.withGroupOrCleanseFunctionOrCompositeCleanseFunction(DqJaxbUtils.getMetaObjectFactory().createCleanseFunctionExtendedDef()
                        .withFunctionName(entry.getValue().get(2))
                        .withJavaClass(entry.getValue().get(1))
                        .withDescription(entry.getValue().get(0))
                        .withCreatedAt(DqJaxbUtils.dateToXMGregorianCalendar(atDate))
                        .withCreatedBy(SecurityUtils.getCurrentUserName()));
            } else if (entry.getKey().startsWith(String.join(".", packagePrefix, "math"))) {
                math.withGroupOrCleanseFunctionOrCompositeCleanseFunction(DqJaxbUtils.getMetaObjectFactory().createCleanseFunctionExtendedDef()
                        .withFunctionName(entry.getValue().get(2))
                        .withJavaClass(entry.getValue().get(1))
                        .withDescription(entry.getValue().get(0))
                        .withCreatedAt(DqJaxbUtils.dateToXMGregorianCalendar(atDate))
                        .withCreatedBy(SecurityUtils.getCurrentUserName()));
            } else if (entry.getKey().startsWith(String.join(".", packagePrefix, "logic"))) {
                logic.withGroupOrCleanseFunctionOrCompositeCleanseFunction(DqJaxbUtils.getMetaObjectFactory().createCleanseFunctionExtendedDef()
                        .withFunctionName(entry.getValue().get(2))
                        .withJavaClass(entry.getValue().get(1))
                        .withDescription(entry.getValue().get(0))
                        .withCreatedAt(DqJaxbUtils.dateToXMGregorianCalendar(atDate))
                        .withCreatedBy(SecurityUtils.getCurrentUserName()));
            } else if (entry.getKey().startsWith(String.join(".", packagePrefix, "convert"))) {
                convert.withGroupOrCleanseFunctionOrCompositeCleanseFunction(DqJaxbUtils.getMetaObjectFactory().createCleanseFunctionExtendedDef()
                        .withFunctionName(entry.getValue().get(2))
                        .withJavaClass(entry.getValue().get(1))
                        .withDescription(entry.getValue().get(0))
                        .withCreatedAt(DqJaxbUtils.dateToXMGregorianCalendar(atDate))
                        .withCreatedBy(SecurityUtils.getCurrentUserName()));
            } else if (entry.getKey().startsWith(String.join(".", packagePrefix, "misc"))) {
                misc.withGroupOrCleanseFunctionOrCompositeCleanseFunction(DqJaxbUtils.getMetaObjectFactory().createCleanseFunctionExtendedDef()
                        .withFunctionName(entry.getValue().get(2))
                        .withJavaClass(entry.getValue().get(1))
                        .withDescription(entry.getValue().get(0))
                        .withCreatedAt(DqJaxbUtils.dateToXMGregorianCalendar(atDate))
                        .withCreatedBy(SecurityUtils.getCurrentUserName()));
            }
        }

        root.withGroupOrCleanseFunctionOrCompositeCleanseFunction(string, math, logic, convert, misc);
        return root;
    }
    */

    /**
     * Process a default cleanse function definition tag.
     *
     * @param functionsMap the map
     * @param propertyId the tag
     * @param k key
     * @param v value
     * @return true, if was a hit, false otherwise
     */
//    private static boolean processCleanseFunctionInfo(Map<String, List<String>> functionsMap, String propertyId, String k, String v) {
//
//        if (k.endsWith(propertyId)) {
//
//            String base = stripAttributePath(getAttributeLevel(k) - 1, k);
//            List<String> values = functionsMap.computeIfAbsent(base, key -> new ArrayList<>(Collections.nCopies(4, null)));
//            switch (propertyId) {
//                case "definition":
//                    values.set(0, StringUtils.trim(v));
//                    break;
//                case "className":
//                    values.set(1, StringUtils.trim(v));
//                    break;
//                case "functionName":
//                    values.set(2, StringUtils.trim(v));
//                    break;
//                case "applicationMode":
//                    values.set(3, StringUtils.trim(v));
//                    break;
//                default:
//                    return false;
//            }
//
//            return true;
//        }
//
//        return false;
//    }
    /**
     * Filter and collect nested entities which used in existing entities (as complex attributes).
     * It will be useful to avoid issues with model export where old non-deleted nested entities exists.
     * See UDSUE-387.
     *
     * @param nestedEntities nested entities collection.
     * @param allEntityDefs all entities from model.
     * @return nested entities referenced from entities.
     */
    public static List<NestedEntity> filterUsageNestedEntities(
            final List<NestedEntity> nestedEntities,
            List<Entity> allEntityDefs
    ) {
        // Get all nestedEntity names used in entities.
        final Set<String> allNestedEntityNames = new HashSet<>();

        allEntityDefs.forEach(entity -> {
            Set<String> nestedNames = getAllNestedEntityNames(nestedEntities, entity);

            allNestedEntityNames.addAll(nestedNames);
        });

        // Collect nestedEntities which referenced from entities in complex attributes.
        return nestedEntities.stream()
                .filter(nestedEntity -> allNestedEntityNames.contains(nestedEntity.getName()))
                .collect(Collectors.toList());

    }

    /**
     * Find all deep nested entity names for declared entity.
     * All deep nested entity tree will be used for search.
     *
     * @param nestedEntities nested entities collection.
     * @param entity entity
     * @return all nested entity names collection.
     */
    private static Set<String> getAllNestedEntityNames(List<NestedEntity> nestedEntities, @Nonnull Entity entity) {
        final Set<String> allNestedEntityNames = new HashSet<>();

        entity.getComplexAttribute().forEach(complexAttributeDef -> {
            allNestedEntityNames.add(complexAttributeDef.getNestedEntityName());

            Set<String> searchNames = Collections.singleton(complexAttributeDef.getNestedEntityName());

            while (true) {
                Set<String> childNames = findChildNestedEntityNames(nestedEntities, searchNames, allNestedEntityNames);

                if (childNames.isEmpty()) {
                    break;
                }

                allNestedEntityNames.addAll(childNames);
                searchNames = childNames;
            }
        });

        return allNestedEntityNames;
    }

    /**
     * Find child nestedEntities in nestedEntities with name in searchNames collection.
     * Only one child level used for search (no deep search used).
     *
     * @param nestedEntities main collection of nested entities to find names.
     * @param searchNames nested entities to search
     * @param ignoreNames ignored names which were found before.
     * @return Set of new nested entities with usages.
     */
    private static Set<String> findChildNestedEntityNames(List<NestedEntity> nestedEntities, Set<String> searchNames,
                                                          Set<String> ignoreNames) {
        Set<String> childNames = new HashSet<>();

        for (String searchName : searchNames) {
            Optional<NestedEntity> nestedEntityOptional = nestedEntities.stream()
                    .filter(nestedEntity -> nestedEntity.getName().equals(searchName)).findFirst();

            if (!nestedEntityOptional.isPresent()) {
                continue;
            }

            NestedEntity targetNestedEntity = nestedEntityOptional.get();

            for (ComplexMetaModelAttribute complexAttr : targetNestedEntity.getComplexAttribute()) {
                // Ignore nested name because it already added in result list.
                // Only new names will be added.
                if (ignoreNames.contains(complexAttr.getNestedEntityName())) {
                    continue;
                }

                childNames.add(complexAttr.getNestedEntityName());
            }
        }

        return childNames;
    }
}
